package org.elasticsearch.plugin;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import org.apache.lucene.search.Query;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.index.query.AbstractQueryBuilder;
import org.elasticsearch.index.query.QueryShardContext;

/** Constructs a query that only match on documents that the field has a value in them. */
public class TimeRangeExistQueryBuilder extends AbstractQueryBuilder<TimeRangeExistQueryBuilder> {
  public static final String NAME = "timeRangeExist";
  public static final ParseField BOOST_FIELD = new ParseField("_value");
  public static final ParseField NAME_FIELD = new ParseField("_name");
  public static final ParseField FIELDS_FIELD = new ParseField("fields");
  private static final ParseField MIN_MATCH_FIELD = new ParseField("minMatch");
  private static final ParseField TIME_INTERVAL_FIELD = new ParseField("timeInterval");

  // filed names, using comma to seperate.
  private final Map<String, String> fieldsValues;
  private final Integer minMatch;
  private final Long timeInterval;

  public TimeRangeExistQueryBuilder(
      Map<String, String> fieldsValues, Integer minMatch, Long timeInterval) {
    if (fieldsValues == null || minMatch == null || timeInterval == null)
      throw new IllegalArgumentException(
          "fieldsValues or minMatch or timeInterval cannot be null.");
    this.minMatch = minMatch;
    this.timeInterval = timeInterval;
    this.fieldsValues = fieldsValues;
  }

  /** Read from a stream. */
  public TimeRangeExistQueryBuilder(StreamInput in) throws IOException {
    super(in);
    minMatch = in.readInt();
    timeInterval = in.readLong();
    int size = in.readVInt();
    fieldsValues = new HashMap<>();
    for (int i = 0; i < size; i++) {
      String field = in.readString();
      String values = in.readString();
      fieldsValues.put(field, values);
    }
  }

  public static TimeRangeExistQueryBuilder fromXContent(XContentParser parser) throws IOException {
    Integer minMatch = null;
    Long timeInterval = null;
    Map<String, String> fieldsValues = new HashMap<>();

    XContentParser.Token token;
    String currentFieldName = null;
    while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
      if (token == XContentParser.Token.FIELD_NAME) {
        currentFieldName = parser.currentName();
      } else if (FIELDS_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
        if (token == XContentParser.Token.START_ARRAY) {
          while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
            parseFieldAndValues(parser, fieldsValues);
          }
        } else if (token.isValue()) {
          parseFieldAndValues(parser, fieldsValues);
        } else {
          throw new ParsingException(
              parser.getTokenLocation(),
              "[" + NAME + "] query does not support [" + currentFieldName + "]");
        }
      } else if (token.isValue()) {
        if (MIN_MATCH_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
          minMatch = parser.intValue();
        } else if (TIME_INTERVAL_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
          timeInterval = parser.longValue();
        } else {
          throw new ParsingException(
              parser.getTokenLocation(),
              "["
                  + TimeRangeExistQueryBuilder.NAME
                  + "] query does not support ["
                  + currentFieldName
                  + "]");
        }
      } else {
        throw new ParsingException(
            parser.getTokenLocation(),
            "["
                + TimeRangeExistQueryBuilder.NAME
                + "] unknown token ["
                + token
                + "] after ["
                + currentFieldName
                + "]");
      }
    }

    TimeRangeExistQueryBuilder builder =
        new TimeRangeExistQueryBuilder(fieldsValues, minMatch, timeInterval);
    return builder;
  }

  private static void parseFieldAndValues(XContentParser parser, Map<String, String> fieldsValues)
      throws IOException {
    String fField = null;
    String fValue = null;
    char[] fieldText = parser.textCharacters();
    int end = parser.textOffset() + parser.textLength();
    for (int i = parser.textOffset(); i < end; i++) {
      if (fieldText[i] == '^') {
        int relativeLocation = i - parser.textOffset();
        fField = new String(fieldText, parser.textOffset(), relativeLocation);
        fValue = new String(fieldText, i + 1, parser.textLength() - relativeLocation - 1);
        break;
      }
    }
    // TODO
    if (fField == null) {
      fField = parser.text();
    }
    fieldsValues.put(fField, fValue);
  }

  @Override
  protected void doWriteTo(StreamOutput out) throws IOException {
    out.writeInt(minMatch);
    out.writeLong(timeInterval);
    out.writeVInt(fieldsValues.size());
    for (Map.Entry<String, String> fieldsEntry : fieldsValues.entrySet()) {
      out.writeString(fieldsEntry.getKey());
      out.writeString(fieldsEntry.getValue());
    }
  }

  @Override
  protected void doXContent(XContentBuilder builder, Params params) throws IOException {
    builder.startObject(NAME);
    builder.startArray(FIELDS_FIELD.getPreferredName());
    for (Map.Entry<String, String> fieldEntry : this.fieldsValues.entrySet()) {
      builder.value(fieldEntry.getKey() + "^" + fieldEntry.getValue());
    }
    builder.endArray();
    builder.field(MIN_MATCH_FIELD.getPreferredName(), minMatch);
    builder.field(TIME_INTERVAL_FIELD.getPreferredName(), timeInterval);
    printBoostAndQueryName(builder);
    builder.endObject();
  }

  @Override
  protected Query doToQuery(QueryShardContext context) {
    return new TimeRangeExistQuery(fieldsValues, minMatch, timeInterval);
  }

  @Override
  protected int doHashCode() {
    return Objects.hash(fieldsValues, minMatch, timeInterval);
  }

  @Override
  protected boolean doEquals(TimeRangeExistQueryBuilder other) {
    return Objects.equals(fieldsValues, other.fieldsValues)
        && Objects.equals(minMatch, other.minMatch)
        && Objects.equals(timeInterval, other.timeInterval);
  }

  @Override
  public String getWriteableName() {
    return NAME;
  }
}
